﻿using System;
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace Borg.Logging
{
    internal sealed class DesktopInvoker : IInvoker
    {
        private static readonly object Lock = new object();
        private const DispatcherPriority DefaultPriority = DispatcherPriority.Normal;

        public event Handler InvocationFinished;
        public event InvokerUnexpectedExceptionEventHandler UnexpectedException;

        private readonly System.Windows.Threading.Dispatcher _dispatcher;

        private bool _notifyInvocationFinished;
        private bool _disposed;

        static DesktopInvoker()
        { }

        private DesktopInvoker()
            : this(System.Windows.Threading.Dispatcher.CurrentDispatcher)
        { }

        private DesktopInvoker(System.Windows.Threading.Dispatcher dispatcher)
        {
            if (dispatcher == null)
                throw new ArgumentNullException("dispatcher");

            _dispatcher = dispatcher;
            _notifyInvocationFinished = true;
            _disposed = false;
            
            Debug.Assert(dispatcher != null);
            
            dispatcher.Hooks.OperationCompleted += OnInvocationFinished;
            dispatcher.Hooks.OperationAborted += OnInvocationFinished;
            
            dispatcher.UnhandledException += OnUnexpectedException;
            dispatcher.ShutdownFinished += InternalDispose;
        }

        public static DesktopInvoker Create()
        {
            var runnable = new Runnable();
            var thread = new Thread(runnable.Run) { Name = "Runtime Desktop" };

            thread.SetApartmentState(ApartmentState.STA);

            lock (Lock)
            {
                thread.Start();
                Monitor.Wait(Lock);
            }

            return new DesktopInvoker(runnable.dispatcher);
        }

        public void Dispose()
        {
            if (_disposed)
                return;
            
            InternalDispose();
        }

        private void InternalDispose(object sender, EventArgs args)
        {
            InternalDispose();
        }

        private void InternalDispose()
        {
            if (_disposed)
                return;

            _dispatcher.Hooks.OperationCompleted -= OnInvocationFinished;
            _dispatcher.Hooks.OperationAborted -= OnInvocationFinished;

            _dispatcher.UnhandledException -= OnUnexpectedException;
            _dispatcher.ShutdownFinished -= InternalDispose;

            _dispatcher.InvokeShutdown();
            _disposed = true;
        }

        private void OnInvocationFinished(object sender, DispatcherHookEventArgs e)
        {
            if (!_notifyInvocationFinished)
                return;

            if (_dispatcher.CheckAccess())
                OnInvocationFinished();
            else
            {
                try
                {
                    _notifyInvocationFinished = false;
                    _dispatcher.Invoke(new Handler(OnInvocationFinished), new object[0]);
                }
                finally
                {
                    _notifyInvocationFinished = true;
                }
            }
        }

        private void OnInvocationFinished()
        {
            Debug.Assert(_dispatcher.CheckAccess());

            if (InvocationFinished == null)
                return;
            
            InvocationFinished();
        }

        private void OnUnexpectedException(object sender, DispatcherUnhandledExceptionEventArgs args)
        {
            args.Handled = true;
            OnUnexpectedException(args.Exception);
        }

        private void OnUnexpectedException(Exception exception)
        {
            if (UnexpectedException == null)
                return;
            
            UnexpectedException(this, exception);
        }

        public object Invoke(Delegate @delegate, params object[] @params)
        {
            if (_disposed)
                throw new InvalidOperationException("Invoker has already been disposed.");

            Debug.Assert(!_dispatcher.HasShutdownFinished);

            var invoker = new Invoker(@delegate, @params);
            System.Windows.Threading.DispatcherOperation dispatcherOperation = null;
            
            try
            {
                Thread.BeginCriticalRegion();

                dispatcherOperation =
                    _dispatcher.BeginInvoke(DefaultPriority, new Action(invoker.Invoke));

                Thread.EndCriticalRegion();

                Debug.Assert(dispatcherOperation.Wait() ==
                    System.Windows.Threading.DispatcherOperationStatus.Completed);

                dispatcherOperation = null;
            }
            finally
            {
                if (dispatcherOperation != null)
                    dispatcherOperation.Abort();
            }

            if (invoker.Exception != null)
                throw invoker.Exception;
            
            return invoker.Result;
        }

        public bool IsInvokingDirectly()
        {
            return false;
        }

        public bool CheckAccess(Dispatcher _)
        {
            return _dispatcher.CheckAccess();
        }

        private sealed class Runnable
        {
            public System.Windows.Threading.Dispatcher dispatcher;

            public void Run()
            {
                lock (Lock)
                {
                    if (Application.Current == null)
                        new Application().ShutdownMode = ShutdownMode.OnExplicitShutdown;

                    dispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
                    Monitor.PulseAll(Lock);
                }

                System.Windows.Threading.Dispatcher.Run();
            }
        }

        private sealed class Invoker
        {
            private readonly Delegate _delegate;
            private readonly object[] _params;

            public object Result { get; private set; }
            public Exception Exception { get; private set; }

            public Invoker(Delegate @delegate, object[] @params)
            {
                _delegate = @delegate;
                _params = @params;
            }

            public void Invoke()
            {
                try { Result = _delegate.DynamicInvoke(_params); }
                catch (Exception ex) { Exception = ex; }
            }
        }
    }
}
